package org.vertexium.cli; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.google.common.collect.Lists; import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.GroovyShell; import jline.TerminalFactory; import jline.UnixTerminal; import jline.UnsupportedTerminal; import jline.WindowsTerminal; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.tools.shell.AnsiDetector; import org.codehaus.groovy.tools.shell.Groovysh; import org.codehaus.groovy.tools.shell.IO; import org.codehaus.groovy.tools.shell.Interpreter; import org.codehaus.groovy.tools.shell.util.Logger; import org.codehaus.groovy.tools.shell.util.NoExitSecurityManager; import org.codehaus.groovy.tools.shell.util.Preferences; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import org.vertexium.Graph; import org.vertexium.GraphFactory; import org.vertexium.VertexiumException; import org.vertexium.Visibility; import org.vertexium.cli.commands.*; import org.vertexium.cli.model.LazyEdgeMap; import org.vertexium.cli.model.LazyVertexMap; import org.vertexium.query.GeoCompare; import org.vertexium.type.GeoPoint; import org.vertexium.util.ConfigurationUtils; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class VertexiumShell { private Groovysh groovysh; @Parameter(names = {"--help", "-h"}, description = "Print help", help = true) private boolean help; @Parameter(names = {"-C"}, description = "Suppress colors") private boolean suppressColor; @Parameter(names = {"-T"}, description = "Terminal type") private String terminalType = TerminalFactory.AUTO; @Parameter(names = {"-e"}, description = "String to evaluate") private String evalString = null; @Parameter(names = {"-c"}, description = "Configuration file name") private List<String> configFileNames = new ArrayList<>(); @Parameter(names = {"-cd"}, description = "Configuration directories (all files ending in .properties)") private List<String> configDirectories = new ArrayList<>(); @Parameter(names = {"-cp"}, description = "Configuration property prefix") private String configPropertyPrefix = null; @Parameter(names = {"-a"}, description = "Authorizations") private String authorizations = null; @Parameter(names = {"-t"}, description = "Time") private Long time = null; @Parameter(names = {"--cypherLabelProperty"}, description = "Cypher label property") private String cypherLabelProperty = null; @Parameter(description = "File names to execute") private List<String> fileNames = new ArrayList<>(); public int run(String[] args) throws Exception { JCommander j = new JCommander(this, args); if (help) { j.usage(); return -1; } setTerminalType(terminalType, suppressColor); addConfigDirectoriesToConfigFileNames(configDirectories, configFileNames); Map<String, String> config = ConfigurationUtils.loadConfig(configFileNames, configPropertyPrefix); Graph graph = new GraphFactory().createGraph(config); System.setProperty("groovysh.prompt", "vertexium"); // IO must be constructed AFTER calling setTerminalType()/AnsiConsole.systemInstall(), // else wrapped System.out does not work on Windows. final IO io = new IO(); Logger.io = io; CliVertexiumCypherQueryContext.setLabelPropertyName(cypherLabelProperty); CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); VertexiumScript.setGraph(graph); if (authorizations != null) { VertexiumScript.setAuthorizations(graph.createAuthorizations(authorizations.split(","))); } VertexiumScript.setTime(time); compilerConfiguration.setScriptBaseClass(VertexiumScript.class.getName()); Binding binding = new Binding(); GroovyShell groovyShell = new GroovyShell(this.getClass().getClassLoader(), binding, compilerConfiguration); Closure<Object> resultHook = new Closure<Object>(this) { @Override public Object call(Object... args) { Object obj = args[0]; boolean showLastResult = !io.isQuiet() && (io.isVerbose() || Preferences.getShowLastResult()); if (showLastResult) { // avoid String.valueOf here because it bypasses pretty-printing of Collections, // e.g. String.valueOf( ['a': 42] ) != ['a': 42].toString() io.out.println("@|bold ===>|@ " + VertexiumScript.resultToString(obj)); } return null; } }; groovysh = new Groovysh(io); setGroovyShell(groovysh, groovyShell); setResultHook(groovysh, resultHook); Groovysh shell = createShell(); shell.execute("import " + Visibility.class.getPackage().getName() + ".*;"); shell.execute("v = new " + LazyVertexMap.class.getName() + "();"); shell.execute("e = new " + LazyEdgeMap.class.getName() + "();"); shell.execute("g = " + VertexiumScript.class.getName() + ".getGraph();"); shell.execute("auths = " + VertexiumScript.class.getName() + ".getAuthorizations();"); shell.execute("time = " + VertexiumScript.class.getName() + ".getTime();"); shell.execute("cypher = { code -> " + VertexiumScript.class.getName() + ".executeCypher(code) };"); startGroovysh(shell, evalString, fileNames); return 0; } private void addConfigDirectoriesToConfigFileNames(List<String> configDirectories, List<String> configFileNames) { for (String configDirectory : configDirectories) { addConfigDirectoryToConfigFileNames(configDirectory, configFileNames); } } private void addConfigDirectoryToConfigFileNames(String configDirectory, List<String> configFileNames) { File dir = new File(configDirectory); if (!dir.exists()) { throw new VertexiumException("Directory does not exist: " + dir.getAbsolutePath()); } List<String> files = Lists.newArrayList(dir.listFiles()).stream() .filter(File::isFile) .map(File::getName) .filter(f -> f.endsWith(".properties")) .collect(Collectors.toList()); Collections.sort(files); files = files.stream() .map(f -> new File(dir, f).getAbsolutePath()) .collect(Collectors.toList()); configFileNames.addAll(files); } private void setGroovyShell(Groovysh groovysh, GroovyShell groovyShell) throws NoSuchFieldException, IllegalAccessException { Field interpField = groovysh.getClass().getDeclaredField("interp"); interpField.setAccessible(true); Field shellField = Interpreter.class.getDeclaredField("shell"); shellField.setAccessible(true); Interpreter interpreter = (Interpreter) interpField.get(groovysh); shellField.set(interpreter, groovyShell); } private void setResultHook(Groovysh groovysh, Closure<Object> resultHook) throws NoSuchFieldException, IllegalAccessException { Field resultHookField = Groovysh.class.getDeclaredField("resultHook"); resultHookField.setAccessible(true); resultHookField.set(groovysh, resultHook); } public Groovysh getGroovysh() { return groovysh; } public static void main(final String[] args) throws Exception { int result = new VertexiumShell().run(args); System.exit(result); } private Groovysh createShell() { final Groovysh shell = getGroovysh(); // Add a hook to display some status when shutting down... Runtime.getRuntime().addShutdownHook(new Thread(() -> { // // FIXME: We need to configure JLine to catch CTRL-C for us... Use gshell-io's InputPipe // if (shell.getHistory() != null) { try { shell.getHistory().flush(); } catch (IOException e) { System.out.println("Could not flush history."); } } })); shell.register(new GetAuthsCommand(shell)); shell.register(new SetAuthsCommand(shell)); shell.register(new GetTimeCommand(shell)); shell.register(new SetTimeCommand(shell)); shell.register(new NowCommand(shell)); return shell; } /** * @param evalString commands that will be executed at startup after loading files given with filenames param * @param filenames files that will be loaded at startup */ protected void startGroovysh(Groovysh shell, String evalString, List<String> filenames) throws IOException { int code; SecurityManager psm = System.getSecurityManager(); System.setSecurityManager(new NoExitSecurityManager()); System.out.println(" _ __ __ _"); System.out.println(" | | / /__ _____/ /____ _ __(_)_ ______ ___"); System.out.println(" | | / / _ \\/ ___/ __/ _ \\| |/_/ / / / / __ `__ \\"); System.out.println(" | |/ / __/ / / /_/ __/> </ / /_/ / / / / / / v" + getClass().getPackage().getImplementationVersion()); System.out.println(" |___/\\___/_/ \\__/\\___/_/|_/_/\\__,_/_/ /_/ /_/"); System.out.println(""); System.out.println("Usage:"); System.out.println(" vertex1=v['vertex1'] - gets the vertex with id 'v1' and assigns it to variable 'v'"); System.out.println(" vertex1.methods - gets the methods available on the Vertexium object"); System.out.println(" vertex1.properties - gets the properties available on the Vertexium object"); System.out.println(" vertex1.delete() - deletes the vertex v1"); System.out.println(" p1.delete() - deletes the property currently referenced by p1"); System.out.println(" q.has('name', 'joe').vertices().each() { println it.id } - execute a query for all vertices with property 'name' equal to 'joe'"); System.out.println(" g.query('apple', auths).vertices()[0] - execute a query for 'apple' and get the first match"); System.out.println(""); System.out.println("Global Properties:"); System.out.println(" g - the Graph object"); System.out.println(" q - a query object"); System.out.println(" auths - the currently set query authorizations"); System.out.println(" time - the currently set query time"); System.out.println(" now - the current time"); System.out.println(" v - vertex map (usage: v['v1'])"); System.out.println(" e - edge map (usage: e['e1'])"); System.out.println(" cypher - run a cypher query (usage: cypher(\"\"\"MATCH (n) RETURN n LIMIT 10\"\"\"))"); try { shell.execute("import " + Visibility.class.getPackage().getName() + ".*;"); shell.execute("import " + GeoPoint.class.getPackage().getName() + ".*;"); shell.execute("import " + GeoCompare.class.getPackage().getName() + ".*;"); code = shell.run(evalString, filenames); } finally { System.setSecurityManager(psm); } // Force the JVM to exit at this point, since shell could have created threads or // popped up Swing components that will cause the JVM to linger after we have been // asked to shutdown System.exit(code); } static void setTerminalType(String type, boolean suppressColor) { assert type != null; type = type.toLowerCase(); boolean enableAnsi = true; switch (type) { case TerminalFactory.AUTO: type = null; break; case TerminalFactory.UNIX: type = UnixTerminal.class.getCanonicalName(); break; case TerminalFactory.WIN: case TerminalFactory.WINDOWS: type = WindowsTerminal.class.getCanonicalName(); break; case TerminalFactory.FALSE: case TerminalFactory.OFF: case TerminalFactory.NONE: type = UnsupportedTerminal.class.getCanonicalName(); // Disable ANSI, for some reason UnsupportedTerminal reports ANSI as enabled, when it shouldn't enableAnsi = false; break; default: // Should never happen throw new IllegalArgumentException("Invalid Terminal type: $type"); } if (enableAnsi) { installAnsi(); // must be called before IO(), since it modifies System.in Ansi.setEnabled(!suppressColor); } else { Ansi.setEnabled(false); } if (type != null) { System.setProperty(TerminalFactory.JLINE_TERMINAL, type); } } static void installAnsi() { // Install the system adapters, replaces System.out and System.err // Must be called before using IO(), because IO stores refs to System.out and System.err AnsiConsole.systemInstall(); // Register jline ansi detector Ansi.setDetector(new AnsiDetector()); } }